require "colorize"
require "json"
require "xml"

require "../src/luce.cr"

def tool_dir : String
  Path[__DIR__].to_s
end

def get_stats_file(prefix : String) : File
  File.open(Path[tool_dir, "#{prefix}_stats.json"], "w")
end

def load_common_mark_sections(test_prefix : String) : Hash(String, Array(CommonMarkTestCase))
  test_file = File.open(Path[tool_dir, "#{test_prefix}_tests.json"])
  tests_json = test_file.gets_to_end

  test_array = Array(JSON::Any).from_json(tests_json).sort! { |a, b| a["section"].as_s <=> b["section"].as_s }

  sections = Hash(String, Array(CommonMarkTestCase)).new

  test_array.each do |example_map|
    example_test = CommonMarkTestCase.from_json(example_map)

    if sections[example_test.section]? == nil
      sections[example_test.section] = [] of CommonMarkTestCase
    end

    sections[example_test.section] << example_test
  end

  sections
end

class Config
  @@common_mark_config = Config.new("common_mark", "https://spec.commonmark.org/0.30/")
  @@gfm_config = Config.new("gfm", "https://github.github.com/gfm/")

  def self.common_mark_config : Config
    @@common_mark_config
  end

  def self.gfm_config : Config
    @@gfm_config
  end

  getter prefix : String
  getter base_url : String

  def initialize(@prefix : String, @base_url : String)
  end
end

class CommonMarkTestCase
  getter markdown : String
  getter section : String
  getter example : Int32
  getter html : String
  getter start_line : Int32
  getter end_line : Int32
  getter extensions : Set(String)

  def initialize(@example, @section, @start_line, @end_line, @markdown, @html, @extensions)
  end

  def to_s : String
    "#{section} - #{example}"
  end

  def self.from_json(json : JSON::Any) : CommonMarkTestCase
    extension_set = Set(String).new

    unless json["extensions"]? == nil
      json["extensions"].as_a.each do |json_any|
        possible_string = json_any.as_s?
        unless possible_string.nil?
          extension_set << possible_string
        end
      end
    end

    CommonMarkTestCase.new(
      json["example"].as_i,
      json["section"].as_s,
      json["start_line"].as_i,
      json["end_line"].as_i,
      json["markdown"].as_s,
      json["html"].as_s,
      extension_set
    )
  end
end

enum CompareLevel
  Strict
  Loose
  Fail
  Error
end

class CompareResult
  getter compare_level : CompareLevel
  getter test_case : CommonMarkTestCase
  getter result : String?

  def initialize(@test_case, @result, @compare_level)
  end
end

def compare_result(config : Config,
                   test_case : CommonMarkTestCase,
                   throw_on_error : Bool = false,
                   verbose_fail : Bool = false,
                   verbose_loose_match : Bool = false,
                   extensions : Set(String) = Set(String).new) : CompareResult
  enable_tag_filter = false

  output : String
  inline_syntaxes = [] of Luce::InlineSyntax
  block_syntaxes = [] of Luce::BlockSyntax

  extensions.each do |extension|
    case extension
    when "autolink"
      inline_syntaxes << Luce::AutolinkExtensionSyntax.new
    when "strikethrough"
      inline_syntaxes << Luce::StrikethroughSyntax.new
    when "table"
      block_syntaxes << Luce::TableSyntax.new
    when "tagfilter"
      enable_tag_filter = true
    else
      raise NotImplementedError.new("Extension #{extension}")
    end
  end

  begin
    output = Luce.to_html(
      test_case.markdown,
      inline_syntaxes: inline_syntaxes,
      block_syntaxes: block_syntaxes,
      enable_tag_filter: enable_tag_filter
    )
  rescue ex
    raise ex if throw_on_error

    print_verbose_failure(config.base_url, "ERROR", test_case, "Thrown: #{err}") if verbose_fail

    return CompareResult.new(test_case, nil, CompareLevel::Error)
  end

  if test_case.html == output
    return CompareResult.new(test_case, output, CompareLevel::Strict)
  end

  # Luce#note(supercell):
  #
  # XML.parse and XML.parse_html require _some_ content
  # otherwise they throw an error. This differs from
  # dart's parseFragment which allows empty Strings.
  html_parser_options = XML::HTMLParserOptions.default | XML::HTMLParserOptions::NODEFDTD | XML::HTMLParserOptions::NOIMPLIED
  if output == "\n"
    expected_parsed = XML.parse_html("\n", html_parser_options)
  else
    expected_parsed = XML.parse_html(test_case.html, html_parser_options)
  end
  actual = XML.parse_html(output, html_parser_options)

  loose_match = compare_html(expected_parsed.children, actual.children)

  if !loose_match && verbose_fail
    print_verbose_failure(config.base_url, "FAIL", test_case, output)
  end

  if loose_match && verbose_loose_match
    print_verbose_failure(config.base_url, "LOOSE", test_case, output)
  end

  CompareResult.new(test_case, output, loose_match ? CompareLevel::Loose : CompareLevel::Fail)
end

def whitespace_color(input : String) : String
  input.gsub(" ", "·".colorize(:light_blue)).gsub("\t", "\t".colorize.back(:dark_gray))
end

def indent(s : String) : String
  s.lines.map { |n| "    #{whitespace_color(n)}" }.join("\n")
end

def print_verbose_failure(base_url : String, message : String, test_case : CommonMarkTestCase, actual : String) : Nil
  puts "#{message}: #{base_url}#example-#{test_case.example} " +
       "@ #{test_case.section}"
  puts "input:"
  puts indent(test_case.markdown)
  puts "expected:"
  puts indent(test_case.html)
  puts "actual:"
  puts indent(actual)
  puts "-----------------------"
end

private def compare_html(expected_elements : XML::NodeSet, actual_elements : XML::NodeSet) : Bool
  return false if expected_elements.size != actual_elements.size

  child_num = 0
  while child_num < expected_elements.size
    expected = expected_elements[child_num]
    actual = actual_elements[child_num]

    return false if expected.class != actual.class

    return false if expected.name != actual.name

    return false if expected.attributes.size != actual.attributes.size

    expected_attribtues_hash = Hash(String, String).new
    expected.attributes.each { |attr| expected_attribtues_hash[attr.name] = attr.content }
    expected_attribute_keys = expected_attribtues_hash.keys.sort!

    actual_attributes_hash = Hash(String, String).new
    actual.attributes.each { |attr| actual_attributes_hash[attr.name] = attr.content }
    actual_attribute_keys = actual_attributes_hash.keys.sort!

    (0...actual_attribute_keys.size).each do |attr_num|
      expected_attribute_key = expected_attribute_keys[attr_num]
      actual_attribute_key = actual_attribute_keys[attr_num]

      if expected_attribute_key != actual_attribute_key
        return false
      end

      if expected_attribtues_hash[expected_attribute_key] !=
           actual_attributes_hash[actual_attribute_key]
        return false
      end
    end

    children_equal = compare_html(expected.children, actual.children)

    return false unless children_equal
    child_num += 1
  end

  true
end
